(function (exports){
    var Protobuf = {};

    Protobuf.init = function(protos) {
        Protobuf.encoder.init(protos.encoderProtos);
        Protobuf.decoder.init(protos.decoderProtos);
    };

    Protobuf.encode = function(key, msg){
        return Protobuf.encoder.encode(key, msg);
    };

    Protobuf.decode = function(key, msg){
        return Protobuf.decoder.decode(key, msg);
    };

    exports.Protobuf = Protobuf;

})(HY.net);

(function (exports){
    var constants = exports.Protobuf.constants = {};

    constants.TYPES = {
        uInt32 : 0,
        sInt32 : 0,
        int32 : 0,
        double : 1,
        string : 2,
        message : 2,
        float : 5
    };

})(HY.net);

(function (exports){

    var Util = exports.Protobuf.util = {};

    Util.isSimpleType = function(type){
        return ( type === 'uInt32' ||
            type === 'sInt32' ||
            type === 'int32'  ||
            type === 'uInt64' ||
            type === 'sInt64' ||
            type === 'float'  ||
            type === 'double' );
    };

})(HY.net);

(function (exports){

    var Codec = exports.Protobuf.codec = {};

    var buffer = new ArrayBuffer(8);
    var float32Array = new Float32Array(buffer);
    var float64Array = new Float64Array(buffer);
    var uInt8Array = new Uint8Array(buffer);

    Codec.encodeUInt32 = function(n){
        var n = parseInt(n);
        if(isNaN(n) || n < 0){
            return null;
        }

        var result = [];
        do{
            var tmp = n % 128;
            var next = Math.floor(n/128);

            if(next !== 0){
                tmp = tmp + 128;
            }
            result.push(tmp);
            n = next;
        }while(n !== 0);

        return result;
    };

    Codec.encodeSInt32 = function(n){
        var n = parseInt(n);
        if(isNaN(n)){
            return null;
        }
        n = n<0?(Math.abs(n)*2-1):n*2;

        return Codec.encodeUInt32(n);
    };

    Codec.decodeUInt32 = function(bytes){
        var n = 0;

        for(var i = 0; i < bytes.length; i++){
            var m = parseInt(bytes[i]);
            n = n + ((m & 0x7f) * Math.pow(2,(7*i)));
            if(m < 128){
                return n;
            }
        }

        return n;
    };


    Codec.decodeSInt32 = function(bytes){
        var n = this.decodeUInt32(bytes);
        var flag = ((n%2) === 1)?-1:1;

        n = ((n%2 + n)/2)*flag;

        return n;
    };

    Codec.encodeFloat = function(float){
        float32Array[0] = float;
        return uInt8Array;
    };

    Codec.decodeFloat = function(bytes, offset){
        if(!bytes || bytes.length < (offset +4)){
            return null;
        }

        for(var i = 0; i < 4; i++){
            uInt8Array[i] = bytes[offset + i];
        }

        return float32Array[0];
    };

    Codec.encodeDouble = function(double){
        float64Array[0] = double;
        return uInt8Array.subarray(0, 8);
    };

    Codec.decodeDouble = function(bytes, offset){
        if(!bytes || bytes.length < (8 + offset)){
            return null;
        }

        for(var i = 0; i < 8; i++){
            uInt8Array[i] = bytes[offset + i];
        }

        return float64Array[0];
    };

    Codec.encodeStr = function(bytes, offset, str){
        for(var i = 0; i < str.length; i++){
            var code = str.charCodeAt(i);
            var codes = encode2UTF8(code);

            for(var j = 0; j < codes.length; j++){
                bytes[offset] = codes[j];
                offset++;
            }
        }

        return offset;
    };

    Codec.decodeStr = function(bytes, offset, length){
        var array = [];
        var end = offset + length;

        while(offset < end){
            var code = 0;

            if(bytes[offset] < 128){
                code = bytes[offset];

                offset += 1;
            }else if(bytes[offset] < 224){
                code = ((bytes[offset] & 0x3f)<<6) + (bytes[offset+1] & 0x3f);
                offset += 2;
            }else{
                code = ((bytes[offset] & 0x0f)<<12) + ((bytes[offset+1] & 0x3f)<<6) + (bytes[offset+2] & 0x3f);
                offset += 3;
            }

            array.push(code);

        }

        var str = '';
        for(var i = 0; i < array.length;){
            str += String.fromCharCode.apply(null, array.slice(i, i + 10000));
            i += 10000;
        }

        return str;
    };

    Codec.byteLength = function(str){
        if(typeof(str) !== 'string'){
            return -1;
        }

        var length = 0;

        for(var i = 0; i < str.length; i++){
            var code = str.charCodeAt(i);
            length += codeLength(code);
        }

        return length;
    };

    function encode2UTF8(charCode){
        if(charCode <= 0x7f){
            return [charCode];
        }else if(charCode <= 0x7ff){
            return [0xc0|(charCode>>6), 0x80|(charCode & 0x3f)];
        }else{
            return [0xe0|(charCode>>12), 0x80|((charCode & 0xfc0)>>6), 0x80|(charCode & 0x3f)];
        }
    }

    function codeLength(code){
        if(code <= 0x7f){
            return 1;
        }else if(code <= 0x7ff){
            return 2;
        }else{
            return 3;
        }
    }
})(HY.net);

(function (exports){

    var protobuf = exports.Protobuf;
    var MsgEncoder = exports.Protobuf.encoder = {};

    var codec = protobuf.codec;
    var constant = protobuf.constants;
    var util = protobuf.util;

    MsgEncoder.init = function(protos){
        this.protos = protos;
    };

    MsgEncoder.encode = function(route, msg){
        //Get protos from protos map use the route as key
        var protos = this.protos[route];

        //Check msg
        if(!checkMsg(msg, protos)){
            return null;
        }

        //Set the length of the buffer 2 times bigger to prevent overflow
        var length = codec.byteLength(JSON.stringify(msg));

        //Init buffer and offset
        var buffer = new ArrayBuffer(length);
        var uInt8Array = new Uint8Array(buffer);
        var offset = 0;

        if(!!protos){
            offset = encodeMsg(uInt8Array, offset, protos, msg);
            if(offset > 0){
                return uInt8Array.subarray(0, offset);
            }
        }

        return null;
    };

    function checkMsg(msg, protos){
        if(!protos){
            return false;
        }

        for(var name in protos){
            var proto = protos[name];

            //All required element must exist
            switch(proto.option){
                case 'required' :
                    if(typeof(msg[name]) === 'undefined'){
                        return false;
                    }
                case 'optional' :
                    if(typeof(msg[name]) !== 'undefined'){
                        if(!!protos.__messages[proto.type]){
                            checkMsg(msg[name], protos.__messages[proto.type]);
                        }
                    }
                    break;
                case 'repeated' :
                    //Check nest message in repeated elements
                    if(!!msg[name] && !!protos.__messages[proto.type]){
                        for(var i = 0; i < msg[name].length; i++){
                            if(!checkMsg(msg[name][i], protos.__messages[proto.type])){
                                return false;
                            }
                        }
                    }
                    break;
            }
        }

        return true;
    }

    function encodeMsg(buffer, offset, protos, msg){
        for(var name in msg){
            if(!!protos[name]){
                var proto = protos[name];;
                switch(proto.option){
                    case 'required' :
                    case 'optional' :
                        offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
                        offset = encodeProp(msg[name], proto.type, offset, buffer, protos);
                        break;
                    case 'repeated' :
                        if(msg[name].length > 0){
                            offset = encodeArray(msg[name], proto, offset, buffer, protos);
                        }
                        break;
                }
            }
        }

        return offset;
    }

    function encodeProp(value, type, offset, buffer, protos){
        switch(type){
            case 'uInt32':
                offset = writeBytes(buffer, offset, codec.encodeUInt32(value));
                break;
            case 'int32' :
            case 'sInt32':
                offset = writeBytes(buffer, offset, codec.encodeSInt32(value));
                break;
            case 'float':
                writeBytes(buffer, offset, codec.encodeFloat(value));
                offset += 4;
                break;
            case 'double':
                writeBytes(buffer, offset, codec.encodeDouble(value));
                offset += 8;
                break;
            case 'string':
                var length = codec.byteLength(value);

                //Encode length
                offset = writeBytes(buffer, offset, codec.encodeUInt32(length));
                //write string
                codec.encodeStr(buffer, offset, value);
                offset += length;
                break;
            default :
                if(!!protos.__messages[type]){
                    //Use a tmp buffer to build an internal msg
                    var tmpBuffer = new ArrayBuffer(codec.byteLength(JSON.stringify(value)));
                    var length = 0;

                    length = encodeMsg(tmpBuffer, length, protos.__messages[type], value);
                    offset = writeBytes(buffer, offset, codec.encodeUInt32(length));
                    for(var i = 0; i < length; i++){
                        buffer[offset] = tmpBuffer[i];
                        offset++;
                    }
                }
                break;
        }

        return offset;
    }

    function encodeArray(array, proto, offset, buffer, protos){
        var i = 0;

        if(util.isSimpleType(proto.type)){
            offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
            offset = writeBytes(buffer, offset, codec.encodeUInt32(array.length));
            for(i = 0; i < array.length; i++){
                offset = encodeProp(array[i], proto.type, offset, buffer);
            }
        }else{
            for(i = 0; i < array.length; i++){
                offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
                offset = encodeProp(array[i], proto.type, offset, buffer, protos);
            }
        }

        return offset;
    }

    function writeBytes(buffer, offset, bytes){
        for(var i = 0; i < bytes.length; i++, offset++){
            buffer[offset] = bytes[i];
        }

        return offset;
    }

    function encodeTag(type, tag){
        var value = constant.TYPES[type]||2;
        return codec.encodeUInt32((tag<<3)|value);
    }
})(HY.net);

(function (exports){
    var protobuf = exports.Protobuf;
    var MsgDecoder = exports.Protobuf.decoder = {};

    var codec = protobuf.codec;
    var util = protobuf.util;

    var buffer;
    var offset = 0;

    MsgDecoder.init = function(protos){
        this.protos = protos || {};
    };

    MsgDecoder.setProtos = function(protos){
        if(!!protos){
            this.protos = protos;
        }
    };

    MsgDecoder.decode = function(route, buf){
        var protos = this.protos[route];

        buffer = buf;
        offset = 0;
        if(!!protos){
            return decodeMsg({}, protos, buffer.length);
        }

        return null;
    };

    function decodeMsg(msg, protos, length){
        while(offset<length){
            var head = getHead();
            var type = head.type;
            var tag = head.tag;
            var name = protos.__tags[tag];

            switch(protos[name].option){
                case 'optional' :
                case 'required' :
                    msg[name] = decodeProp(protos[name].type, protos);
                    break;
                case 'repeated' :
                    if(!msg[name]){
                        msg[name] = [];
                    }
                    decodeArray(msg[name], protos[name].type, protos);
                    break;
            }
        }

        return msg;
    }

    function isFinish(msg, protos){
        return (!protos.__tags[peekHead().tag]);
    }

    function getHead(){
        var tag = codec.decodeUInt32(getBytes());

        return {
            type : tag&0x7,
            tag : tag>>3
        };
    }

    function peekHead(){
        var tag = codec.decodeUInt32(peekBytes());

        return {
            type : tag&0x7,
            tag : tag>>3
        };
    }

    function decodeProp(type, protos){
        switch(type){
            case 'uInt32':
                return codec.decodeUInt32(getBytes());
            case 'int32' :
            case 'sInt32' :
                return codec.decodeSInt32(getBytes());
            case 'float' :
                var float = codec.decodeFloat(buffer, offset);
                offset += 4;
                return float;
            case 'double' :
                var double = codec.decodeDouble(buffer, offset);
                offset += 8;
                return double;
            case 'string' :
                var length = codec.decodeUInt32(getBytes());

                var str =  codec.decodeStr(buffer, offset, length);
                offset += length;

                return str;
            default :
                if(!!protos && !!protos.__messages[type]){
                    var length = codec.decodeUInt32(getBytes());
                    var msg = {};
                    decodeMsg(msg, protos.__messages[type], offset+length);
                    return msg;
                }
                break;
        }
    }

    function decodeArray(array, type, protos){
        if(util.isSimpleType(type)){
            var length = codec.decodeUInt32(getBytes());

            for(var i = 0; i < length; i++){
                array.push(decodeProp(type));
            }
        }else{
            array.push(decodeProp(type, protos));
        }
    }

    function getBytes(flag){
        var bytes = [];
        var pos = offset;
        flag = flag || false;

        var b;

        do{
            b = buffer[pos];
            bytes.push(b);
            pos++;
        }while(b >= 128);

        if(!flag){
            offset = pos;
        }
        return bytes;
    }

    function peekBytes(){
        return getBytes(true);
    }

})(HY.net);

(function (exports) {

    exports.Protobuf.Parser = {};
    var Parser = exports.Protobuf.Parser;

    Parser.parse = function(protos){
        var maps = {};
        for(var key in protos){
            maps[key] = parseObject(protos[key]);
        }

        return maps;
    }

    function parseObject(obj) {
        var proto = {};
        var nestProtos = {};
        var tags = {};

        for(var name in obj) {
            var tag = obj[name];
            var params = name.split(' ');

            switch(params[0]) {
                case 'message':
                    if(params.length != 2)
                        continue;
                    nestProtos[params[1]] = parseObject(tag);
                    continue;
                case 'required':
                case 'optional':
                case 'repeated': {
                    //params length should be 3 and tag can't be duplicated
                    if(params.length != 3 || !!tags[tag]){
                        continue;
                    }
                    proto[params[2]] = {
                        option : params[0],
                        type : params[1],
                        tag : tag
                    }
                    tags[tag] = params[2];
                }
            }
        }

        proto.__messages = nestProtos;
        proto.__tags = tags;
        return proto;
    }

})(HY.net);
